import EventEmiter from "./EventEmiter";

function getData(source) {
    const o = target.get(this);
    if(!o.audioContext) {
        o.audioContext = new (AudioContext || webkitAudioContext)();
        o.gainNode = o.audioContext.createGain();
        o.analyser = o.audioContext.createAnalyser();
        o.analyser.fftSize = o.fftSize;
    }
    return new Promise((resolve, reject) => {
        const index = o.cache.findIndex(d => d.source === source);
        if(index > -1) {
            resolve(o.cache[index]);
        } else {
            const temp = {source, buffer: null, loaded: false};
            if(o.cache.length >= o.cacheMax) o.cache.shift();
            o.cache.push(temp);
            fetch(source).then(r =>  r.arrayBuffer()).then(arrayBuf => {
                o.audioContext.decodeAudioData(arrayBuf, buffer => {
                    temp.loaded = true;
                    temp.buffer = buffer;
                    resolve(temp);
                }, reject);
            }).catch(reject);
        }
    });
}

function play(buffer) {
    const o = target.get(this);
    if(!o.isPlaying) return;
    o.audioSource = o.audioContext.createBufferSource();
    o.audioSource.onended = () => {
        o.isPlaying = false;
        o.end = this.duration;
        this.emit('end')
        this.emit('over')
    };
    o.audioSource.buffer = buffer;
    o.audioSource.connect(o.gainNode);
    o.gainNode.gain.value = o.volume;
    o.gainNode.connect(o.analyser);
    o.analyser.connect(o.audioContext.destination);
    o.start = o.audioContext.currentTime;
    o.audioSource.loop = o.loop;
    o.audioSource.start(0, o.end);
}

function getSource() {
    const o = target.get(this);
    const index = o.cache.findIndex(d => d.source === o.source);
    if(index > -1) return o.cache[index];
    return {loaded: false, buffer: null};
}

const options = {
    loop: false,
    fftSize: 256,
    volume: 1,
    cacheMax: 10,
    onEnd() {}
};

const target = new WeakMap();

class Audio extends EventEmiter {
    constructor(ops) {
        super()
        ops = Object.assign({}, options, ops);
        target.set(this, {
            isPlaying: false,
            volume: ops.volume,
            loop: ops.loop,
            cacheMax: ops.cacheMax,
            fftSize: ops.fftSize,
            cache: [],
            start: 0,
            end: 0,
            isSupported: !!(AudioContext || webkitAudioContext)
        });
    }
    get isSupported() {
        return target.get(this).isSupported;
    }
    get getByteFrequencyData() {
        if(!getSource.call(this).buffer) return new Uint8Array(0);
        const o = target.get(this);
        const buffer = new Uint8Array(o.analyser.frequencyBinCount);
        o.analyser.getByteFrequencyData(buffer);
        return buffer;
    }
    get getFloatFrequencyData() {
        if(!getSource.call(this).buffer) return new Float32Array(0);
        const o = target.get(this);
        const buffer = new Float32Array(o.analyser.frequencyBinCount);
        o.analyser.getFloatFrequencyData(buffer);
        return buffer;
    }
    get source() {
        return target.get(this).source || '';
    }
    get volume() {
        return target.get(this).volume;
    }
    get isLoop() {
        return target.get(this).loop;
    }
    get isPlaying() {
        return target.get(this).isPlaying;
    }
    get duration() {
        if(!getSource.call(this).buffer) return 0;
        return getSource.call(this).buffer.duration;
    }
    get currentTime() {
        if(!getSource.call(this).loaded) return 0;
        const o = target.get(this);
        if(o.isPlaying) {
            o.end += o.audioContext.currentTime - o.start;
            o.start = o.audioContext.currentTime;
            if(o.end >= this.duration) o.end -= this.duration;
        }
        return o.end;
    }
    setVolume(n) {
        const o = target.get(this);
        n = Math.max(0, Math.min(1, parseFloat(n)));
        o.volume = n;
        if(o.gainNode) o.gainNode.gain.value = n;
    }
    toggleLoop() {
        const o = target.get(this);
        o.loop = !o.loop;
        if(o.audioSource) o.audioSource.loop = o.loop;
    }
    preload(source) {
        const o = target.get(this);
        if(o.cache.findIndex(d => d.source === source) > -1) return;
        getData.call(this, source).then(r => {
            if(o.source === source && o.processPreload) {
                o.processPreload(r);
                o.processPreload = null;
            }
        });
    }
    play(source) {
        const o = target.get(this);
        if(o.isPlaying && o.source === source) return;
        o.isPlaying = true;
        if(o.end === this.duration) o.end = 0;
        if(o.audioSource && o.source !== source) {
            o.start = 0;
            o.end = 0;
            o.audioSource.onended = null;
            o.audioSource.stop();
        }
        o.source = source;
        getData.call(this, source).then(r => {
            if(o.source === source) {
                if(!r.buffer) {
                    o.processPreload = (r) => {
                        play.call(this, r.buffer);
                    };
                } else {
                    o.processPreload = null;
                    play.call(this, r.buffer);
                }
            }
        });
    }
    seek(time) {
        time |= 0;
        if(time > this.duration || time < 0) return;
        const o = target.get(this);
        if(o.audioSource) {
            o.audioSource.onended = null;
            o.audioSource.stop();
            getData.call(this, target.get(this).source).then(r => {
                o.isPlaying = true;
                o.end = time;
                play.call(this, r.buffer);
            });
        }
    }
    pause() {
        const o = target.get(this);
        if(!o.isPlaying) return;
        o.isPlaying = false;
        if(o.audioSource) {
            o.end += o.audioContext.currentTime - o.start;
            if(o.end >= this.duration) {
                o.end -= this.duration;
            }
            o.audioSource.onended = null;
            o.audioSource.stop();
        }
    }
    stop() {
        const o = target.get(this);
        if(!o.isPlaying) return;
        o.isPlaying = false;
        o.start = 0;
        o.end = 0;
        if(o.audioSource) {
            o.audioSource.onended = null;
            o.audioSource.stop();
            this.emit('stop')
            this.emit('over')
        }
    }
    destroy() {
        super.destroy()
        const o = target.get(this);
        if(o.audioContext) o.audioContext.close();
        target.delete(this);
    }
}

export default Audio;